/*
 * Copyright 2011 Clean GWT
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.cleangwt.bootstrap.client.ui;

import java.util.ArrayList;
import java.util.List;

import com.cleangwt.bootstrap.client.style.Forms;
import com.cleangwt.uibinder.client.TemplateSupport;
import com.cleangwt.user.client.Elements;
import com.cleangwt.user.client.ui.RequiresOptions;
import com.cleangwt.user.shared.Option;
import com.cleangwt.user.shared.Options;
import com.cleangwt.validation.client.Checks;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.LabelElement;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Widget;

/**
 * A check box group provides string value that composed from multi selection, the value may like
 * 'value1, value2, value3...', each value is just packed up with the {@link Options#FORM_DELIMITER}
 * . For correct representation, you had better not add any option value that contains the
 * {@link Options#FORM_DELIMITER}.
 * 
 * @author Jess
 * @version 1.0
 * @since 1.0
 */
public class CheckField extends Widget implements HasValue<String>, RequiresOptions {

  /**
   * Creates a check field that wraps an existing &lt;div&gt; with &lt;label&gt; and
   * &lt;checkbox&gt; elements of template.
   * 
   * @param template the template
   * @param element the element to be wrapped
   */
  public static CheckField wrap(TemplateSupport template, Element element) {
    CheckField widget = new CheckField(element);
    template.wrap(widget);
    return widget;
  }

  private boolean inline;
  private String lastValue;
  private boolean valueChangeHandlerInitialized;

  /**
   * Creates a check field.
   */
  public CheckField() {
    this(true);
  }

  /**
   * Creates a check field with the specified inline style.
   * 
   * @param inline the inline style
   * 
   */
  public CheckField(boolean inline) {
    setElement(Document.get().createDivElement());
    setInline(inline);
  }

  /**
   * This constructor may be used by subclasses to explicitly use an existing element. This element
   * must be a &lt;div&gt; element.
   * 
   * @param element the element to be used
   */
  protected CheckField(Element element) {
    DivElement.as(element);
    setElement(element);
    NodeList<Element> labels = element.getElementsByTagName(LabelElement.TAG);
    if (labels.getLength() > 0) {
      this.inline = Elements.hasClass(Forms.CHECKBOX_INLINE_STYLE, labels.getItem(0));
    }
  }

  /**
   * Adds an option.
   * 
   * @param label the name
   * @param value the value
   */
  public void addOption(String label, String value) {
    Element lbl = Document.get().createLabelElement();
    InputElement check = Document.get().createCheckInputElement();
    check.setValue(value);
    lbl.setInnerHTML(label);
    lbl.insertFirst(check);
    if (inline) {
      this.getElement().appendChild(lbl);
      lbl.addClassName(Forms.CHECKBOX_INLINE_STYLE);
    } else {
      Element div = Document.get().createDivElement();
      this.getElement().appendChild(div);
      div.appendChild(lbl);
      div.setClassName(Forms.CHECKBOX_STYLE);
    }
  }

  /**
   * Adds a selection of the given option value.
   * 
   * @param optionValue the option values
   */
  public void addSelection(String optionValue) {
    addSelection(optionValue, false);
  }

  /**
   * Selects an option of value with given text, Fires
   * com.google.gwt.event.logical.shared.ValueChangeEvent when fireEvents is true and the new value
   * does not equal the existing value. If no option exist for the value, add one.
   * 
   * @param value the value to set
   * @param text the text to present
   * @param fireEvents whether to fire events
   */
  public void addSelection(String value, boolean fireEvents) {
    NodeList<Element> inputs = this.getElement().getElementsByTagName(InputElement.TAG);
    for (int i = 0; i < inputs.getLength(); i++) {
      InputElement e = InputElement.as(inputs.getItem(i));
      if (e.getValue().equals(value)) {
        e.setChecked(true);
        break;
      }
    }

    if (fireEvents) {
      ValueChangeEvent.fire(this, getValue());
    }
  }

  @Override
  public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) {
    if (!valueChangeHandlerInitialized) {
      valueChangeHandlerInitialized = true;
      initializeValueChangeHandler();
    }
    return addHandler(handler, ValueChangeEvent.getType());
  }

  /**
   * Resets selections.
   * 
   * @param fireEvents whether to fire events
   */
  public void clearSelection(boolean fireEvents) {
    NodeList<Element> inputs = this.getElement().getElementsByTagName(InputElement.TAG);
    for (int i = 0; i < inputs.getLength(); i++) {
      InputElement e = InputElement.as(inputs.getItem(i));
      e.setChecked(false);
    }
    if (fireEvents) {
      ValueChangeEvent.fire(this, getValue());
    }
  }

  /**
   * Retrieves selections.
   * 
   * @return the text of all selected box
   */
  public List<String> getSelections() {
    List<String> selections = new ArrayList<String>();
    NodeList<Element> inputs = this.getElement().getElementsByTagName(InputElement.TAG);
    for (int i = 0; i < inputs.getLength(); i++) {
      InputElement e = InputElement.as(inputs.getItem(i));
      if (e.isChecked()) {
        selections.add(e.getValue());
      }
    }
    return selections;
  }

  @Override
  public String getValue() {
    List<String> selections = new ArrayList<String>();
    NodeList<Element> inputs = this.getElement().getElementsByTagName(InputElement.TAG);
    for (int i = 0; i < inputs.getLength(); i++) {
      InputElement e = InputElement.as(inputs.getItem(i));
      if (e.isChecked()) {
        selections.add(e.getValue());
      }
    }
    return Options.form(selections);
  }

  private void initializeValueChangeHandler() {
    this.addDomHandler(new ClickHandler() {

      @Override
      public void onClick(ClickEvent event) {
        ValueChangeEvent.fire(CheckField.this, getValue());
      }
    }, ClickEvent.getType());
  }

  /**
   * Removes a selection of the given option value.
   * 
   * @param optionValue the option value
   * @param fireEvents whether to fire events
   */
  public void removeSelection(String optionValue, boolean fireEvents) {
    NodeList<Element> inputs = this.getElement().getElementsByTagName(InputElement.TAG);
    for (int i = 0; i < inputs.getLength(); i++) {
      InputElement e = InputElement.as(inputs.getItem(i));
      if (e.getValue().equals(optionValue)) {
        e.setChecked(false);
        break;
      }
    }

    if (fireEvents) {
      ValueChangeEvent.fire(this, getValue());
    }
  }

  /**
   * Selects all.
   * 
   * @param fireEvents whether to fire events
   */
  public void selectAll(boolean fireEvents) {
    NodeList<Element> inputs = this.getElement().getElementsByTagName(InputElement.TAG);
    for (int i = 0; i < inputs.getLength(); i++) {
      InputElement e = InputElement.as(inputs.getItem(i));
      e.setChecked(true);
    }

    if (fireEvents) {
      ValueChangeEvent.fire(this, getValue());
    }
    if (fireEvents) {
      ValueChangeEvent.fire(this, getValue());
    }
  }

  /**
   * Sets the inline style.
   * 
   * @param inline the inline style
   */
  public void setInline(boolean inline) {
    this.inline = inline;
    // if (inline) {
    // this.setStyleName(INLINE_STYLE);
    // } else {
    // this.setStyleName(DEFAULT_STYLENAME);
    // }
  }

  @Override
  public void setOptions(List<Option> options) {
    setOptions(options, true);
  }

  /**
   * Sets the options and keeps the selected option when keepSelection is true.
   * 
   * @param options the options
   * @param reset whether to reset the last value(the value may be set before options are added to
   *          this check field)
   * @see #setValue(String,boolean)
   */
  public void setOptions(List<Option> options, boolean reset) {
    getElement().setInnerText("");
    for (Option option : options) {
      addOption(option.getName(), option.getValue());
    }
    if (reset) {
      lastValue = null;
    } else {
      setValue(lastValue);
    }
  }

  @Override
  public void setValue(String value) {
    this.setValue(value, false);
  }

  /**
   * Sets the value. If the options of this check field have not been set, keep the value. When the
   * {@link #setOptions(List, boolean)} is called, it will recheck the option of value in this check
   * field.
   * 
   * @param value the value to set
   * @param fireEvents whether to fire events
   * @see #setOptions(List, boolean)
   */
  @Override
  public void setValue(String value, boolean fireEvents) {
    this.lastValue = value;
    clearSelection(false);
    String oldValue = getValue();
    if (!Checks.isSame(oldValue, value)) {
      if (value != null) {
        String[] selections = Options.unform(value);
        NodeList<Element> inputs = this.getElement().getElementsByTagName(InputElement.TAG);
        for (String selection : selections) {
          for (int i = 0; i < inputs.getLength(); i++) {
            InputElement option = inputs.getItem(i).cast();
            if (selection.equals(option.getValue())) {
              option.setChecked(true);
              break;
            }
          }
        }
      }
      if (fireEvents) {
        ValueChangeEvent.fire(this, value);
      }
    }
  }
}
